#include "driverjack.h"
//#include "datatypes.h"
//#include "enginehandler.h"
#include <cstdio>
#include <QStringList>
#include <unistd.h>
#include <QWaitCondition>
#include "driverjack.h"
#include "qkeymetro.h"

//#include "pool.h"
//#include <math.h>

using namespace DJ;


struct DriverJack::AudioPortStruct {
    DJ::Sample ** buffer;
    bool isOutput;
    jack_port_t * jackPort;
    QStringList connectedPortsNames;
};


struct DriverJack::StaticData {
    static int 		process(jack_nframes_t nframes, void *unused_arg);
    static int 		srate (jack_nframes_t nframes, void *unused_arg);
    static void 	error (const char *desc);
    static void 	jack_shutdown (void *unused_arg);
    static int 		bufsize_callback (jack_nframes_t  nframes, void *unused_arg);

    jack_nframes_t 		sampleRate;
    jack_nframes_t 		bufferSize;
    jack_client_t * 	client;
    AudioPortStruct audioPortsList [DJ::MAX_N_PORTS];

    int NPortsRegistered;
    QWaitCondition buffersReady;

    void (*setBufferSize) (int size);
    void (*setSampleRate) (int sampleRate);
};

DriverJack::StaticData DriverJack::staticData_;

QWaitCondition & DriverJack::getWaitCondition(){
    return staticData_.buffersReady;
}

// ====================  STATIC SECTION (CALLBACKS) BEGIN
//
int DriverJack::StaticData::process (jack_nframes_t nFrames, void *unused_arg)
{
    Q_UNUSED(unused_arg);
    DJ::Sample * tmpBuffer;
    AudioPortStruct * audioPort;
    for (int i = 0; i < DJ::MAX_N_PORTS; i++) {
        audioPort = staticData_.audioPortsList + i;
        if (audioPort->buffer != NULL &&
            audioPort->jackPort != NULL) {
            tmpBuffer = (DJ::Sample *) jack_port_get_buffer (audioPort->jackPort, nFrames);
            if (audioPort->isOutput) {
                memcpy(tmpBuffer, *(audioPort->buffer), sizeof(DJ::Sample)*nFrames);
            } else {
                memcpy(*(audioPort->buffer), tmpBuffer, sizeof(DJ::Sample)*nFrames);
            }
        }
    }
    staticData_.buffersReady.wakeAll();
    return 0;
}

int DriverJack::StaticData::srate (jack_nframes_t nFrames, void *unused_arg)
{
    Q_UNUSED(unused_arg);
    fprintf (stderr, "JD: now the sample rate is %lu/sec\n", (long unsigned int)nFrames);
    staticData_.sampleRate = nFrames;
    return 0;
}

void DriverJack::StaticData::error (const char *desc)
{
    fprintf (stderr, "JD: JACK error: %s\n", desc);
}

void DriverJack::StaticData::jack_shutdown (void *unused_arg)
{
    Q_UNUSED(unused_arg);
 //   fprintf (stderr, "JD: JACK server is shuting down! \n");
    //exit (1);
}

int DriverJack::StaticData::bufsize_callback (jack_nframes_t  nFrames, void *unused_arg)
{
    Q_UNUSED(unused_arg);
    fprintf (stderr, "JD: buffer size is now %lu/sec\n", (long unsigned int)nFrames);
    staticData_.bufferSize = nFrames;
    staticData_.setBufferSize(nFrames);
    return 0;
}
//
// ====================  STATIC SECTION (CALLBACKS) END
//
DriverJack::DriverJack(const char * nameOfClient) :
        driverState_(CREATED),
        nameOfClient_(nameOfClient),
        nameOfDriver_ (DJ::NAME_OF_DRIVER_JACK)
{
    staticData_.NPortsRegistered = 0;
    staticData_.bufferSize = 0;
    staticData_.client = NULL;
    QKeyMetro::StaticData::log("JD: ...construction ok ");
}

DJ::ReturnCode DriverJack::init(void (*setBufferSize)(int), void (*setSampleRate)(int))
{
    if (driverState_ != CREATED) {
        QKeyMetro::StaticData::log(QString( "JD: cannot init driver - not created state"));
        return -1;
    }
    QKeyMetro::StaticData::log("JD: initialization started...");

    if (nameOfClient_.size() == 0) {
        QKeyMetro::StaticData::log(QString("JD: jack client name is not defined"));
        return -1;
    }

    jack_set_error_function (StaticData::error);
    // open client, register callbacks, actvate client
    if ((staticData_.client = jack_client_open (nameOfClient_.toAscii().data(), JackNullOption, NULL)) == 0) {
        QKeyMetro::StaticData::log(QString("JD: jack server not running?\n"));
        return -1;
    }
    jack_set_process_callback (staticData_.client, StaticData::process, 0);
    QKeyMetro::StaticData::log(QString("..set_process_callback"));
    jack_set_sample_rate_callback (staticData_.client, StaticData::srate, 0);
    QKeyMetro::StaticData::log(QString("..set_sample_rate_callback"));
//    jack_on_shutdown (staticData_.client, StaticData::jack_shutdown, 0);  // doesnt work on WIN should be reconsidered
//    QKeyMetro::StaticData::log(QString("..on_shutdown"));
    jack_set_buffer_size_callback (staticData_.client, StaticData::bufsize_callback, 0);
    QKeyMetro::StaticData::log(QString("..buffer_size_callback"));

    staticData_.sampleRate = jack_get_sample_rate (staticData_.client);
    staticData_.bufferSize = jack_get_buffer_size (staticData_.client);

    staticData_.setBufferSize = setBufferSize;
    staticData_.setSampleRate = setSampleRate;
    staticData_.setBufferSize(staticData_.bufferSize);
    staticData_.setSampleRate(staticData_.sampleRate);

    QKeyMetro::StaticData::log(QString("JD: sample rate : %1, buffer size : %2 ").arg(staticData_.sampleRate).arg(staticData_.bufferSize));

    for (int i = 0; i < DJ::MAX_N_PORTS; i++) {
        staticData_.audioPortsList[i].jackPort = NULL;
        staticData_.audioPortsList[i].buffer = NULL;
    }
    driverState_ = INITED;
    return 0;
}

DriverJack::~DriverJack()
{
    QKeyMetro::StaticData::log("JD: exiting...");
    if (driverState_ != STOPED )
        if (stop() != 0) {QKeyMetro::StaticData::log("JD: proper deconstruction failed - cant stop");
    };

    QKeyMetro::StaticData::log("JD: cleanup...");


    AudioPortStruct * ap; // saving connections of client
    for (int i = 0; i < DJ::MAX_N_PORTS; i++) {
        ap = & (staticData_.audioPortsList[i]);
        if (! ap->connectedPortsNames.isEmpty()) ap->connectedPortsNames.clear();
    }

    QKeyMetro::StaticData::log("JD: ...closing client ");

    if (staticData_.client != NULL) jack_client_close (staticData_.client);
    //usleep(100000);
    QKeyMetro::StaticData::log("JD: ...deconstruction ok ");
}

DJ::ReturnCode DriverJack::start()
{
    if (! (driverState_ == STOPED || driverState_ == INITED)) {
        QKeyMetro::StaticData::log(QString( "JD: cannot start driver - not stoped or inited"));
        return -1;//DJ::JACK_CANT_ACTIVATE;
    }
    QKeyMetro::StaticData::log("JD: starting jack client...");

    if (staticData_.client == NULL ) {
        QKeyMetro::StaticData::log(QString( "JD: cannot activate client"));
        return -1;//DJ::JACK_CANT_ACTIVATE;
    }

    if (jack_activate (staticData_.client) == 0) {
        QKeyMetro::StaticData::log(QString( "JD: client is activated and started"));
        driverState_ = STARTED;
        return 0;//DJ::SUCCESS;
    } else {
        QKeyMetro::StaticData::log(QString( "JD: client cant be activated, unknown error")) ;
        return -1;//DJ::JACK_CANT_ACTIVATE;
    }
}

DJ::ReturnCode DriverJack::stop()
{
    if (driverState_ != STARTED ) {
        QKeyMetro::StaticData::log(QString( "JD: cannot stop driver, it has been not started yet, but trying to deactivate anyway"));
        return -1; // DJ::JACK_CANT_ACTIVATE;
    }
    QKeyMetro::StaticData::log("JD: stopping jack client...");
    if (staticData_.client == NULL /*|| ! staticData_.isClientActive*/ ) {
        QKeyMetro::StaticData::log(QString( "JD: nothing to deactivate - no client (NULL pointer)"));
        return -1;//DJ::JACK_CANT_DEACTIVATE;
    }

    if (jack_deactivate (staticData_.client) == 0) {
        //staticData_.isClientActive = false;
        QKeyMetro::StaticData::log(QString( "JD: client is deactivated"));
        driverState_ = STOPED;
        return 0; //DJ::SUCCESS;
    }
    QKeyMetro::StaticData::log(QString( "JD: client can not be deactivated"));
    return -1;//DJ::JACK_CANT_DEACTIVATE;
}

const QString & DriverJack::getNameOfDriver()
{
    return nameOfDriver_;
}

QStringList * DriverJack::getPhysicalPortNames(bool isJackOutput, bool isAudio)
{
    unsigned long jpf =  JackPortIsPhysical |
                         (isJackOutput ? JackPortIsOutput : JackPortIsInput);
    const char ** physicalPorts;
    if ((physicalPorts = jack_get_ports (staticData_.client, NULL, NULL, jpf)) == NULL) {
        QKeyMetro::StaticData::log("JD: can not find any physical ports");
        return NULL;
    }
    QStringList * ports = new QStringList();

    for (int i = 0; physicalPorts[i] != NULL; i++) {
        if (QString((isAudio ? JACK_DEFAULT_AUDIO_TYPE : JACK_DEFAULT_MIDI_TYPE)) ==
            QString(jack_port_type(jack_port_by_name (staticData_.client, physicalPorts[i])))
            ) {
            ports->append(QString(physicalPorts[i]));
            QKeyMetro::StaticData::log("JD: physical port found: " + QString(physicalPorts[i]));
        }
    }
    jack_free(physicalPorts);
    return ports;
}


int DriverJack::getBufferSize() {return staticData_.bufferSize;}

int DriverJack::registerPort(DJ::Sample ** buffer, bool isOutput, QString name)
{
    QKeyMetro::StaticData::log("JD: registering port : <"+ name + ">...");
    if (staticData_.NPortsRegistered == DJ::MAX_N_PORTS ||
        staticData_.client == NULL ||
        buffer == NULL) {
        QKeyMetro::StaticData::log("JD: can not register port");
        return -1;
    }
    int portID = -1;
    for (int i = 0; i < DJ::MAX_N_PORTS; i++) {
        if (staticData_.audioPortsList[i].jackPort == NULL &&
            staticData_.audioPortsList[i].buffer == NULL) {
            portID = i;
            break;
        }
    }
    if (portID == -1 ) {
        QKeyMetro::StaticData::log("JD: number of ports exeeded maximum");
        return -1; // free record not found
    }
    if (name.isEmpty()) name =
            QString(isOutput ? AUDIO_OUT_PORT_PREFIX : AUDIO_IN_PORT_PREFIX).arg(
                    portID, N_DIGITS_IN_A_PORT_NAME, 10, QChar('0'));

    jack_port_t * jackPort = jack_port_register (staticData_.client,
                                                 name.toLatin1().data(),
                                                 JACK_DEFAULT_AUDIO_TYPE,
                                                 (isOutput ? JackPortIsOutput : JackPortIsInput),
                                                 0);

    if (jackPort == NULL) {
        QKeyMetro::StaticData::log("JD: port registration was rejected by jack server");
        return -1;
    }

    staticData_.NPortsRegistered++;
    staticData_.audioPortsList[portID].buffer = buffer;
    staticData_.audioPortsList[portID].isOutput = isOutput;
    staticData_.audioPortsList[portID].jackPort = jackPort;
    QKeyMetro::StaticData::log("JD: port is registered : " + name);
    return portID;
}

QString DriverJack::setPortName(int portID, const QString & portName)
{
    if (staticData_.audioPortsList[portID].jackPort == NULL ||
        portName.isEmpty()) {
        QKeyMetro::StaticData::log("JD: cant change portname to : " + portName);
        return "";
    }

    jack_port_set_name	(	staticData_.audioPortsList[portID].jackPort,
                                portName.toLatin1().data());
    QKeyMetro::StaticData::log("JD: new port name is : " +  QString(jack_port_short_name(staticData_.audioPortsList[portID].jackPort)));
    return jack_port_short_name(staticData_.audioPortsList[portID].jackPort);
}

DJ::ReturnCode DriverJack::deletePort(const int portID)
{
    stop();
    if (jack_port_unregister (
            staticData_.client, staticData_.audioPortsList[portID].jackPort))	{
        staticData_.audioPortsList[portID].buffer = NULL;
        staticData_.audioPortsList[portID].jackPort = NULL;
        start();
        return -1; //DJ::SUCCESS;
    }
    start();
    return -1; //DJ::JACK_CANT_UNREGISTER_PORT;
}

QStringList * DriverJack::getMyPortNames()
{
    QStringList * myPortNames = new QStringList();
    for (int i = 0; i < MAX_N_PORTS; i++) {
        if (staticData_.audioPortsList[i].jackPort != NULL) {
            myPortNames->append(
                    QString(jack_port_short_name(staticData_.audioPortsList[i].jackPort)));
        }
    }
    return myPortNames;
}

DJ::ReturnCode	DriverJack::connectPort(const int myPortID, const QString & extPortName)

{
    if (driverState_ != STARTED ) {
        QKeyMetro::StaticData::log(QString( "JD: can't connect to port: " + extPortName + ", start driver first!"));
        return -1;//DJ::JACK_CANT_CONNECT_PORT;
    }

    QString source;
    QString destination;

    if (staticData_.audioPortsList[myPortID].isOutput) {
        source = jack_port_name(staticData_.audioPortsList[myPortID].jackPort);
        destination = extPortName;
    } else {
        destination = jack_port_name(staticData_.audioPortsList[myPortID].jackPort);
        source = extPortName;
    }

    if (jack_connect (staticData_.client,source.toAscii().data(), destination.toAscii().data()) == 0)
    {
        QKeyMetro::StaticData::log("JD: ports connected: " + source + " to: " + destination);
        return 0; //DJ::SUCCESS;
    } else {
        QKeyMetro::StaticData::log("JD: can't connect " + source + " to: " + destination);
        return -1;//DJ::JACK_CANT_CONNECT_PORT;
    }

}

DJ::ReturnCode	DriverJack::connectDefaultPorts()
{
    // connecting first two inputs of the host to two physical outputs of the card
    // and vice versa, to provide basic operation mode
    QKeyMetro::StaticData::log("JD: connecting defaults..");
    int nOutputs = 0;
    int nInputs = 0;
    QStringList * cardOuts = getPhysicalPortNames(false, true);// card's out = jackd's in
    QStringList * cardIns = getPhysicalPortNames(true, true);
    for (int i = 0; i < MAX_N_PORTS; i++) {
        if (staticData_.audioPortsList[i].jackPort != NULL) {
            jack_port_disconnect(staticData_.client, staticData_.audioPortsList[i].jackPort);
            if (staticData_.audioPortsList[i].isOutput) {
                if (nOutputs < 2 && (cardOuts->size() - nOutputs) > 0) {
                    connectPort(i, (*cardOuts)[nOutputs]);
                    nOutputs++;
                }
            }	else {
                if (nInputs < 2 && (cardIns->size() - nInputs) > 0) {
                    connectPort(i, (*cardIns)[nInputs]);
                    nInputs++;
                }
            }
        }
    }
    QKeyMetro::StaticData::log("JD: connecting defaults done.");
    delete cardIns;
    delete cardOuts;
    return 0;//DJ::SUCCESS;
}
